﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using CrmClient.Mapping;
using CrmClient.Query;
using CrmClient.QueryConvert;
using Microsoft.Xrm.Sdk.Query;

namespace CrmClient.Linq
{
    public static class Linq
    {
        #region Select

        private static CrmQueryable<R> CrmSelect_inner<T, R>(CrmQueryable<T> src, LambdaExpression expression)
        {
            var bindings = GetExpressionBindings(expression.Body);
            var propertiesMap = GetPropertiesMap(bindings, b => MappingUtils.GetCrmAttributeName((MemberExpression)b.Argument, src.PropertiesMap));
            return src.Clone<R>(src.EntityName, propertiesMap);
        }

        public static CrmQueryable<R> CrmSelect<T, R>(this CrmQueryable<T> src, Expression<Func<T, R>> expression)
        {
            return (CrmQueryable<R>)CrmSelect_inner<T, R>(src, expression);
        }
        public static CrmJoinedQueryable<R> CrmSelect<T, R>(this CrmJoinedQueryable<T> src, Expression<Func<T, R>> expression)
        {
            return (CrmJoinedQueryable<R>)CrmSelect_inner<T, R>(src, expression);
        }

        #endregion

        #region Order

        private static CrmQueryable<T> CrmOrderBy_inner<T, P>(this CrmQueryable<T> src, Expression<Func<T, P>> prop, OrderType orderType)
        {
            if (!(prop.Body is MemberExpression))
                throw new ArgumentException("Параметр prop должен указывать на свойство по которому будет сортироваться результат.");
            var res = src.Clone<T>(src.EntityName, src.PropertiesMap);
            var crmAttrName = MappingUtils.GetCrmAttributeName((MemberExpression)prop.Body, src.PropertiesMap);
            res.AddSortOrder(new OrderExpression(crmAttrName.ToString(), OrderType.Ascending));
            return res;
        }

        public static CrmQueryable<T> CrmOrderBy<T, P>(this CrmQueryable<T> src, Expression<Func<T, P>> prop)
        {
            return (CrmQueryable<T>)CrmOrderBy_inner(src, prop, OrderType.Ascending);
        }
        public static CrmQueryable<T> CrmOrderByDescending<T, P>(this CrmQueryable<T> src, Expression<Func<T, P>> prop)
        {
            return (CrmQueryable<T>)CrmOrderBy_inner(src, prop, OrderType.Descending);
        }

        #endregion

        #region Where

        private static object GetValue(Expression expression)
        {
            if (expression is ConstantExpression)
                return ((ConstantExpression)expression).Value;
            else
                return LambdaExpression.Lambda(expression).Compile().DynamicInvoke();
        }
        private static FilterExpression ConvertLinqWhereExpressionToCrmExpression<T>(BinaryExpression expression, PropertiesMap propertiesMap)
        {
            var type = typeof(T);
            var res = new FilterExpression();
            if (expression.Left is MemberExpression || expression.Right is MemberExpression)
            {
                MemberExpression member;
                object constant;
                if (expression.Left is MemberExpression && ((MemberExpression)expression.Left).Member.ReflectedType.IsOrBasedOn(type.BaseType))
                {
                    member = (MemberExpression)expression.Left;
                    constant = GetValue(expression.Right);
                }
                else if (((MemberExpression)expression.Right).Member.ReflectedType.IsOrBasedOn(type.BaseType))
                {
                    member = (MemberExpression)expression.Right;
                    constant = GetValue(expression.Left);
                }
                else
                    throw new ArgumentException(string.Format("В выражении {0} ни один член не являются свойством типа {1}", expression, type.Name));
                var attrName = MappingUtils.GetCrmAttributeName(member, propertiesMap);
                res.Conditions.Add(new ConditionExpression(attrName.ToString(), NodeTypeToConditionOperator(expression.NodeType), constant));
            }
            else
            {
                res.FilterOperator = NodeTypeToLogicalOperator(expression.NodeType);
                res.AddFilter(ResolveExpression<T>(expression.Left, propertiesMap));
                res.AddFilter(ResolveExpression<T>(expression.Right, propertiesMap));
            }
            return res;
        }
        private static FilterExpression ConvertLinqWhereExpressionToCrmExpression<T>(MethodCallExpression expression, PropertiesMap propertiesMap, bool not)
        {
            var res = new FilterExpression();
            if (expression.Method.DeclaringType == typeof(string))
            {
                string pattern;
                if (expression.Method.Name == "StartsWith")
                    pattern = "{0}%";
                else if (expression.Method.Name == "EndsWith")
                    pattern = "%{0}";
                else if (expression.Method.Name == "Contains")
                    pattern = "%{0}%";
                else
                    throw new ArgumentException("expression");
                var attrName = MappingUtils.GetCrmAttributeName((MemberExpression)expression.Object, propertiesMap);
                res.Conditions.Add(new ConditionExpression(attrName.ToString(), not ? ConditionOperator.NotLike : ConditionOperator.Like, string.Format(pattern, GetValue(expression.Arguments[0]))));
            }
            else if (expression.Method.Name == "Contains")
            {
                if (expression.Method.DeclaringType != typeof(System.Linq.Enumerable))
                    throw new NotSupportedException("В условии In можно использовать только Enumerable (попробуйте использовать ToArray)");
                var list = GetValue(expression.Arguments[0]) as ICollection;
                var attrName = MappingUtils.GetCrmAttributeName((MemberExpression)expression.Arguments[1], propertiesMap);
                res.Conditions.Add(new ConditionExpression(attrName.ToString(), not ? ConditionOperator.NotIn : ConditionOperator.In, list));
            }
            else
                throw new NotSupportedException("expression");
            return res;
        }
        private static FilterExpression ConvertLinqWhereExpressionToCrmExpression<T>(UnaryExpression expression, PropertiesMap propertiesMap)
        {
            if (expression.Operand is MethodCallExpression)
                return ConvertLinqWhereExpressionToCrmExpression<T>((MethodCallExpression)expression.Operand, propertiesMap, expression.NodeType == ExpressionType.Not);
            else if (expression.Operand is BinaryExpression)
                return ConvertLinqWhereExpressionToCrmExpression<T>((BinaryExpression)expression.Operand, propertiesMap);
            else if (expression.Operand is UnaryExpression)
                return ConvertLinqWhereExpressionToCrmExpression<T>((UnaryExpression)expression.Operand, propertiesMap);
            throw new ArgumentException("expression");
        }
        private static FilterExpression ResolveExpression<T>(Expression expression, PropertiesMap propertiesMap)
        {
            if(expression is BinaryExpression)
                return ConvertLinqWhereExpressionToCrmExpression<T>((BinaryExpression)expression, propertiesMap);
            else if (expression is MethodCallExpression)
                return ConvertLinqWhereExpressionToCrmExpression<T>((MethodCallExpression)expression, propertiesMap, false);
            else if (expression is UnaryExpression)
                return ConvertLinqWhereExpressionToCrmExpression<T>((UnaryExpression)expression, propertiesMap);
            else
                throw new ArgumentException("expression");
        }

        public static CrmQueryable<T> CrmWhere<T>(this CrmQueryable<T> src, Expression<Func<T, bool>> expression)
        {
            var filter = ResolveExpression<T>(expression.Body, src.PropertiesMap);
            var res = src.Clone<T>(src.EntityName, src.PropertiesMap);
            res.AddFilter(filter);
            return (CrmQueryable<T>)res;
        }

        #endregion

        #region Join

        private static CrmJoinedQueryable<R> CrmJoin_inner<S, D, P1, P2, R>(CrmQueryable<S> src, CrmQueryable<D> dst, Expression<Func<S, P1>> srcProp, Expression<Func<D, P2>> dstProp, 
            Expression<Func<S, D, R>> res, JoinOperator joinType)
        {
            if (!(srcProp.Body is MemberExpression) || !(dstProp.Body is MemberExpression))
                throw new ArgumentException("Параметры srcProp и dstProp должны указывать на свойства для связки сущностей");

            var bindings = GetExpressionBindings(res.Body);

            var dstBindings = bindings.Where(b => b.Argument is MemberExpression && ((ParameterExpression)((MemberExpression)b.Argument).Expression).Name == ((ParameterExpression)((MemberExpression)dstProp.Body).Expression).Name);
            var srcBindings = bindings.Except(dstBindings);

            var srcCrmAttrName = MappingUtils.GetCrmAttributeName((MemberExpression)srcProp.Body, src.PropertiesMap);
            var dstCrmAttrName = MappingUtils.GetCrmAttributeName((MemberExpression)dstProp.Body, dst.PropertiesMap);

            var linkCrmAttrs = new List<string>();
            foreach (var binding in dstBindings)
                linkCrmAttrs.Add(MappingUtils.GetCrmAttributeName((MemberExpression)binding.Argument, dst.PropertiesMap).ToString());

            CrmLinkAlias alias;
            var resQuery = src.ConvertToJoin<R>(dst.EntityName, a => GetPropertiesMap(bindings, b =>
            {
                if (srcBindings.Contains(b))
                    return MappingUtils.GetCrmAttributeName((MemberExpression)b.Argument, src.PropertiesMap);
                return MappingUtils.GetCrmAttributeName((MemberExpression)b.Argument, dst.PropertiesMap, a.ToString());
            }), out alias);
            resQuery.AddLink(alias, srcCrmAttrName.ToString(), dstCrmAttrName.ToString(), linkCrmAttrs, joinType, dst.Filter);
            return resQuery;
        }

        public static CrmJoinedQueryable<R> CrmJoin<S, D, P, R>(this CrmQueryable<S> src, CrmQueryable<D> dst, Expression<Func<S, P>> srcProp, Expression<Func<D, P>> dstProp, Expression<Func<S, D, R>> res)
        {
            return CrmJoin_inner(src, dst, srcProp, dstProp, res, JoinOperator.Inner);
        }
        public static CrmJoinedQueryable<R> CrmJoin<S, D, P, R>(this CrmJoinedQueryable<S> src, CrmQueryable<D> dst, Expression<Func<S, P>> srcProp, Expression<Func<D, P>> dstProp, Expression<Func<S, D, R>> res)
        {
            return CrmJoin_inner(src, dst, srcProp, dstProp, res, JoinOperator.Inner);
        }
        public static CrmJoinedQueryable<R> CrmLeftJoin<S, D, P, R>(this CrmQueryable<S> src, CrmQueryable<D> dst, Expression<Func<S, P>> srcProp, Expression<Func<D, P>> dstProp, Expression<Func<S, D, R>> res)
        {
            return CrmJoin_inner(src, dst, srcProp, dstProp, res, JoinOperator.LeftOuter);
        }
        public static CrmJoinedQueryable<R> CrmLeftJoin<S, D, P, R>(this CrmJoinedQueryable<S> src, CrmQueryable<D> dst, Expression<Func<S, P>> srcProp, Expression<Func<D, P>> dstProp, Expression<Func<S, D, R>> res)
        {
            return CrmJoin_inner(src, dst, srcProp, dstProp, res, JoinOperator.LeftOuter);
        }
        
        #endregion

        #region Distinct

        private static CrmQueryable<T> CrmDistinct_inner<T>(CrmQueryable<T> src)
        {
            var res = src.Clone<T>(src.EntityName, src.PropertiesMap);
            res.SetDistinct(true);
            return res;
        }

        public static CrmQueryable<T> CrmDistinct<T>(this CrmQueryable<T> src)
        {
            return (CrmQueryable<T>)CrmDistinct_inner(src);
        }
        public static CrmJoinedQueryable<T> CrmDistinct<T>(this CrmJoinedQueryable<T> src)
        {
            return (CrmJoinedQueryable<T>)CrmDistinct_inner(src);
        }

        #endregion

        #region NoLock

        private static CrmQueryable<T> CrmNoLock_inner<T>(this CrmQueryable<T> src)
        {
            var res = src.Clone<T>(src.EntityName, src.PropertiesMap);
            res.SetNoLock(true);
            return res;
        }

        public static CrmQueryable<T> CrmNoLock<T>(this CrmQueryable<T> src)
        {
            return (CrmQueryable<T>)CrmNoLock_inner(src);
        }
        public static CrmJoinedQueryable<T> CrmNoLock<T>(this CrmJoinedQueryable<T> src)
        {
            return (CrmJoinedQueryable<T>)CrmNoLock_inner(src);
        }

        #endregion

        #region Utils

        private class ExpressionBinding
        {
            public Expression Argument { get; private set; }

            public MemberInfo Member { get; private set; }

            public ExpressionBinding(Expression argument, MemberInfo member)
            {
                Argument = argument;
                Member = member;
            }

            public override int GetHashCode()
            {
                return unchecked(23 * Argument.GetHashCode() * Member.GetHashCode());
            }

            public override bool Equals(object obj)
            {
                bool res = false;
                if (obj is ExpressionBinding)
                    res = ((ExpressionBinding)obj).Argument == this.Argument && ((ExpressionBinding)obj).Member == this.Member;
                return res;
            }
        }

        private static ConditionOperator NodeTypeToConditionOperator(ExpressionType exprType)
        {
            switch (exprType)
            {
                case ExpressionType.Equal:
                    return ConditionOperator.Equal;
                case ExpressionType.LessThan:
                    return ConditionOperator.LessThan;
                case ExpressionType.LessThanOrEqual:
                    return ConditionOperator.LessEqual;
                case ExpressionType.GreaterThan:
                    return ConditionOperator.GreaterThan;
                case ExpressionType.GreaterThanOrEqual:
                    return ConditionOperator.GreaterEqual;
            }
            throw new ArgumentException(string.Format("Для ExpressionType '{0}' не задан соответствующий ConditionOperator", exprType));
        }

        private static LogicalOperator NodeTypeToLogicalOperator(ExpressionType exprType)
        {
            switch (exprType)
            {
                case ExpressionType.Or:
                case ExpressionType.OrElse:
                    return LogicalOperator.Or;
                case ExpressionType.And:
                case ExpressionType.AndAlso:
                    return LogicalOperator.And;
            }
            throw new ArgumentException(string.Format("Для ExpressionType '{0}' не задан соответствующий LogicalOperator", exprType));
        }

        private static List<ExpressionBinding> GetExpressionBindings(Expression body)
        {
            var res = new List<ExpressionBinding>();
            if (body is NewExpression) //Анонимный тип
            {
                var arguments = ((NewExpression)body).Arguments;
                var members = ((NewExpression)body).Members;
                if (arguments.Count != members.Count)
                    throw new Exception("В анонимном типе кол-во свойств не равно кол-ву параметров.(Сломался .net?)");
                for (int i = 0; i < arguments.Count; i++)
                    res.Add(new ExpressionBinding(arguments[i], members[i]));
            }
            else if (body is MemberInitExpression) //Обычный тип
            {
                foreach (var binding in ((MemberInitExpression)body).Bindings)
                    res.Add(new ExpressionBinding(((MemberAssignment)binding).Expression, binding.Member));
            }
            else
                throw new ArgumentException("expression");
            return res;
        }

        private static PropertiesMap GetPropertiesMap(List<ExpressionBinding> bindings, Func<ExpressionBinding, CrmAttrName> crmAttrNameResolver)
        {
            var propertiesMap = new PropertiesMap();
            foreach (var binding in bindings)
                if (binding.Argument is MemberExpression)
                {
                    var crmAttrName = crmAttrNameResolver(binding);
                    var type = ((PropertyInfo)((MemberExpression)binding.Argument).Member).PropertyType;
                    propertiesMap.Add(new PropertyMap(crmAttrName, binding.Member.Name, type));
                }
                else if (binding.Argument is ConstantExpression)
                    propertiesMap.Add(new PropertyMap(binding.Member.Name, ((PropertyInfo)binding.Member).PropertyType, ((ConstantExpression)binding.Argument).Value));
                else
                    throw new ArgumentException("argument");
            return propertiesMap;
        }

        private static bool IsOrBasedOn(this Type curtype, Type type)
        {
            if (curtype == type || curtype.BaseType == type)
                return true;
            if (curtype.BaseType == typeof(object) && curtype.BaseType != type)
                return false;
            return curtype.BaseType.IsOrBasedOn(type);
        }
        
        #endregion
    }
}
